#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */ 
#endregion

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlTypes;
using System.Xml;
using Adoor.Expression;
using Adoor.Expression.AST;
using Adoor.Object.Domain;
using Adoor.Object.Entity;

namespace Adoor.Object.Relational
{
	public interface IPropertyMappingStrategy
	{
		ExpressionNode GetRelationalExpression(string correlation);
		void InsertRelationalColumn(IList<SelectExpression> columns, string correlation);
	}

	public abstract class PropertyFactoryBase: IPropertyMapping, IPropertyReader, IPropertyMappingStrategy
	{
		private string columnName;
		private int maxLength;
		private PropertyData propertyData;

		protected PropertyFactoryBase(PropertyData propertyData, XmlNode metadata)
		{
			this.propertyData = propertyData;

			XmlNode columnNode = metadata.SelectSingleNode("@column");
			this.columnName = columnNode == null ? this.PropertyData.Name : columnNode.Value;
									
			XmlNode lengthNode = metadata.SelectSingleNode("@length");
			this.maxLength = lengthNode != null ? Convert.ToInt32(lengthNode.Value) : 0;

			this.PropertyData.RegisterService(typeof(IPropertyMapping), this);
			this.PropertyData.RegisterService(typeof(IPropertyReader), this);
			this.PropertyData.RegisterService(typeof(IPropertyMappingStrategy), this);
		}

		public PropertyData PropertyData
		{
			get { return this.propertyData; }
		}

		public string ColumnName
		{
			get { return this.columnName; }
		}

		public int MaxLength
		{
			get{ return maxLength; }
		}

		public ExpressionNode GetRelationalExpression(string correlation)
		{
			return new ExpressionName(correlation, this.ColumnName);
		}

        public void InsertRelationalColumn(IList<SelectExpression> columns, string correlation)
		{
			columns.Add(new SelectExpression(this.PropertyData.Name, new ExpressionName(correlation, this.ColumnName)));
		}

		public object GetObject(IEntity sourceEntity)
		{
			return ((IRelationalEntityState)sourceEntity.State)[this.ColumnName];
		}

		public String GetString(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return "";
			return Convert.ToString(value);
		}

		public SqlString GetSqlString(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlString.Null;
			return Convert.ToString(value);
		}

		public Boolean GetBoolean(IEntity sourceEntity)
		{
			return Convert.ToBoolean(this.GetObject(sourceEntity));
		}

		public SqlBoolean GetSqlBoolean(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlBoolean.Null;
			return Convert.ToBoolean(value);
		}

		public Int32 GetInt32(IEntity sourceEntity)
		{
			return Convert.ToInt32(this.GetObject(sourceEntity));
		}

		public SqlInt32 GetSqlInt32(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlInt32.Null;
			return Convert.ToInt32(value);
		}

		public Int16 GetInt16(IEntity sourceEntity)
		{
			return Convert.ToInt16(this.GetObject(sourceEntity));
		}

		public SqlInt16 GetSqlInt16(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlInt16.Null;
			return Convert.ToInt16(value);
		}

		public Double GetDouble(IEntity sourceEntity)
		{
			return Convert.ToDouble(this.GetObject(sourceEntity));
		}

		public SqlDouble GetSqlDouble(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlDouble.Null;
			return Convert.ToDouble(value);
		}

		public Single GetSingle(IEntity sourceEntity)
		{
			return Convert.ToSingle(this.GetObject(sourceEntity));
		}

		public SqlSingle GetSqlSingle(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value is DBNull)
				return SqlSingle.Null;
			return Convert.ToSingle(value);
		}

		public DateTime GetDateTime(IEntity sourceEntity)
		{
			return Convert.ToDateTime(this.GetObject(sourceEntity));
		}

		public SqlDateTime GetSqlDateTime(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value == DBNull.Value)
				return SqlDateTime.Null;
			return Convert.ToDateTime(value);
		}
		
		public Decimal GetDecimal(IEntity sourceEntity)
		{
			return Convert.ToDecimal(this.GetObject(sourceEntity));
		}

		public SqlDecimal GetSqlDecimal(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value == DBNull.Value)
				return SqlDecimal.Null;
			return Convert.ToDecimal(value);
		}

		public Byte[] GetByteArray(IEntity sourceEntity)
		{
			return (byte[])Convert.ChangeType(this.GetObject(sourceEntity), typeof(byte[]));
		}

		public SqlGuid GetSqlGuid(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if (value == DBNull.Value)
				return SqlGuid.Null;
			if(value is SqlGuid)
				return (SqlGuid) value;
			if(value is Guid)
				return new SqlGuid( (Guid) value);
			return new SqlGuid(value.ToString());			
		}

		public Guid GetGuid(IEntity sourceEntity)
		{
			object value = this.GetObject(sourceEntity);
			if(value is SqlGuid)
				return ((SqlGuid) value).Value;
			if(value is Guid)
				return (Guid) value;
			return new Guid(value.ToString());
		}

		public void InsertDataColumn(IList<DataColumn> dataColumns)
		{
			dataColumns.Add(CreateDataColumn());
		}

		protected virtual DataColumn CreateDataColumn()
		{
			Type columnType = this.PropertyData.PropertyInfo.PropertyType;
			if (columnType.IsEnum)
				columnType = typeof(int);
			DataColumn dc = new DataColumn(this.ColumnName, columnType);
			dc.AllowDBNull = true; // TODO: read from XML
			dc.DefaultValue = DBNull.Value;
			dc.ReadOnly = true;
			if (this.maxLength != 0)
				dc.MaxLength = this.maxLength;
			return dc;
		}
	}

	public class PropertyFactory: PropertyFactoryBase, IPropertyWriter
	{
		private bool readOnly;

		public PropertyFactory(PropertyData propertyData, XmlNode metadata): 
			base(propertyData, metadata)
		{
			XmlNode readOnlyNode = metadata.SelectSingleNode("@readOnly");
			this.readOnly = readOnlyNode == null ? false : Convert.ToBoolean(readOnlyNode.Value);

			this.PropertyData.RegisterService(typeof(IPropertyWriter), this);
		}

		protected override DataColumn CreateDataColumn()
		{
			DataColumn result = base.CreateDataColumn();
			result.ReadOnly = this.readOnly;
			return result;
		}

		public void SetObject(IEntity sourceEntity, object value)
		{
			((IRelationalEntityState)sourceEntity.State)[this.ColumnName] = value;
		}

		public void SetString(IEntity sourceEntity, String value)
		{
			string patchedValue = value;			
			if(MaxLength > 0 && patchedValue.Length > MaxLength)
				patchedValue = patchedValue.Substring(0, MaxLength);
			if (patchedValue.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, patchedValue);
		}

		public void SetSqlString(IEntity sourceEntity, SqlString value)
		{
			SqlString cVal = GetSqlString(sourceEntity);
			if(cVal.IsNull == value.IsNull)
				return;

			string cStr = cVal.Value;
			
			string patchedValue = value.Value;			
			if(MaxLength > 0 && patchedValue.Length > MaxLength)
				patchedValue = patchedValue.Substring(0, MaxLength);

			if (patchedValue.Equals(cStr))
				return;
			this.SetObject(sourceEntity, patchedValue);
		}

		public void SetBoolean(IEntity sourceEntity, Boolean value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlBoolean(IEntity sourceEntity, SqlBoolean value)
		{
			if (value.Equals(GetSqlBoolean(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.IsTrue);
		}

		public void SetInt32(IEntity sourceEntity, Int32 value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlInt32(IEntity sourceEntity, SqlInt32 value)
		{
			if (value.Equals(GetSqlInt32(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetInt16(IEntity sourceEntity, Int16 value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlInt16(IEntity sourceEntity, SqlInt16 value)
		{
			if (value.Equals(GetSqlInt16(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetDouble(IEntity sourceEntity, Double value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlDouble(IEntity sourceEntity, SqlDouble value)
		{
			if (value.Equals(GetSqlDouble(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetSingle(IEntity sourceEntity, Single value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlSingle(IEntity sourceEntity, SqlSingle value)
		{
			if (value.Equals(GetSqlSingle(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetDateTime(IEntity sourceEntity, DateTime value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlDateTime(IEntity sourceEntity, SqlDateTime value)
		{
			if (value.Equals(GetSqlDateTime(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetDecimal(IEntity sourceEntity, Decimal value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlDecimal(IEntity sourceEntity, SqlDecimal value)
		{
			if (value.Equals(GetSqlDecimal(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value.Value);
		}

		public void SetByteArray(IEntity sourceEntity, Byte[] value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetGuid(IEntity sourceEntity, Guid value)
		{
			if (value.Equals(this.GetObject(sourceEntity)))
				return;
			this.SetObject(sourceEntity, value);
		}

		public void SetSqlGuid(IEntity sourceEntity, SqlGuid value)
		{
			if (value.Equals(GetSqlGuid(sourceEntity)))
				return;
			if (value.IsNull)
				this.SetObject(sourceEntity, DBNull.Value);
			else
				this.SetObject(sourceEntity, value);
		}
	}

	public class SubqueryPropertyMapper : IPropertyMappingStrategy
	{
		public SubqueryPropertyMapper(SelectExpression selectExp)
		{
			this.selectExp = selectExp;
		}

		private SelectExpression selectExp;

		public ExpressionNode GetRelationalExpression(string correlation)
		{
			return new ExpressionName(correlation, this.selectExp.Alias);
		}

        public void InsertRelationalColumn(IList<SelectExpression> columns, string correlation)
		{
			columns.Add(this.selectExp);
		}		
	}
}
